package it.unibo.imola.parser.visitor.entity;

import it.unibo.imola.utils.ToStringUtils;

import java.util.ArrayList;

import org.apache.commons.lang3.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class CallStatement extends Statement {

	private Logger logger = LoggerFactory.getLogger(getClass());
	protected String name;
	protected ToolBox toolbox;
	protected ArrayList<Object> actualParametersList;

	public CallStatement(String name, ToolBox toolbox) {
		if (name == null)
			throw new IllegalArgumentException("name == null");
		if (toolbox == null)
			throw new IllegalArgumentException("toolbox == null");
		this.name = name;
		this.toolbox = toolbox;
		actualParametersList = new ArrayList<Object>(0);
	}

	public void addActualParameter(Object param) {
		if (param == null)
			throw new IllegalArgumentException("param == null");
		actualParametersList.add(param);
	}

	@Override
	public boolean check(Environment env) throws Exception {
		super.check(env);

		Class<?>[] actualParameterType = new Class<?>[actualParametersList.size()];

		// creo l'array di tipi per vedere se esiste un metodo con questa
		// signature
		// cioè se corrisponde alla definizione dei parametri formali
		//
		// devo cercare sia tra le funzioni che ho definito, sia nella toolbox

		for (int i = 0; i < actualParameterType.length; i++) {
			if (actualParametersList.get(i) instanceof String) {
				String parameter = actualParametersList.get(i).toString();
				if (parameter.startsWith("\"")) { // è una stringa costante
					actualParameterType[i] = String.class;
					continue;
				} else {
					if (!env.getVariables().containsKey(parameter)) {
						throw new Exception(name + ": check: Undeclared variable \"" + parameter + "\"");
					}
					Variable var = env.getVariables().get(parameter);
					actualParameterType[i] = var.getValue().getClass();
					continue;
				}

			}
			// qui ci arriva sempre se non è un nome di una variabile
			actualParameterType[i] = actualParametersList.get(i).getClass();
		}

		// qui vado prima a vedere se c'è una definizione con quel nome e con
		// quei parametri
		if (env instanceof GlobalEnvironment) {
			GlobalEnvironment gEnv = (GlobalEnvironment) env;
			if (gEnv.getDefinitions().containsKey(name)) {
				StatementSequence statementSequence = gEnv.getDefinitions().get(name);
				Class<?>[] formalParameterTypes = statementSequence.getDefinition().getFormalParameterTypes();
				boolean compatible = true;
				for (int i = 0; i < formalParameterTypes.length; i++) {
					if (formalParameterTypes[i] != actualParameterType[i]) {
						if (logger.isDebugEnabled())
							logger.debug("formal[" + i + "]=" + formalParameterTypes[i].getSimpleName() + " not equal to actual[" + i + "]=" + actualParameterType[i].getSimpleName());
						compatible = false;
					}
				}

				if (compatible){
					return true;
				}
			}
		}

		if (logger.isDebugEnabled())
			logger.debug(name + ": check:  GlobalEnvironment does not contains DEFINITION \"" + name + "\"");

		// qui vado a vedere se c'è un metodo nella toolbox con quel nome e con
		// quei parametri
		if (!toolbox.exists(name, actualParameterType)) {
			if (logger.isDebugEnabled())
				logger.debug(getClass().getSimpleName() + " does not contains CALL \"" + name + "\" " + ArrayUtils.toString(actualParameterType) + " ");

			throw new Exception(name + ": check: Undefined CALL \"" + name + "\" " + ToStringUtils.toString(actualParameterType));
		}
		return true;

	}

	@Override
	public void exec(Environment env) throws Exception {
		super.exec(env);

		// creo un array con i parametri "unboxed" quindi con solo costanti.
		// per fare questo, se il parametro attuale è una costante, resta uguale
		// se invece è una variabile, ci metto una copia del valore attuale

		Object[] unboxedActualParameters = new Object[actualParametersList.size()];

		for (int i = 0; i < unboxedActualParameters.length; i++) {
			if (actualParametersList.get(i) instanceof String) {
				String parameter = actualParametersList.get(i).toString();

				if (parameter.startsWith("\"")) { // è una stringa costante
					unboxedActualParameters[i] = parameter.substring(1, parameter.length() - 1);
				} else {
					if (!env.getVariables().containsKey(parameter)) {
						throw new Exception(name + ": exec: Undeclared variable \"" + parameter + "\"");
					}
					Variable var = env.getVariables().get(parameter);
					unboxedActualParameters[i] = var.getValue();
				}
			} else
				unboxedActualParameters[i] = actualParametersList.get(i);
		}

		// adesso ho tutti i miei parametri attuali "unboxed", e se ho fatto la
		// check() sono sicuro
		// che o nel GlobalEnv o nella ToolBox il metodo da chiamare c'è.
		// vado prima a vedere se la call fa riferimento ad una funzione
		// definita nello script

		if (env instanceof GlobalEnvironment) {
			GlobalEnvironment gEnv = (GlobalEnvironment) env;
			if (gEnv.getDefinitions().containsKey(name)) {
				StatementSequence statementSequence = gEnv.getDefinitions().get(name);
				statementSequence.setActualParameters(unboxedActualParameters);
				statementSequence.exec(gEnv);
				return;
			}
		}

		if (logger.isDebugEnabled())
			logger.debug(name + ": exec: GlobalEnvironment does not contains DEFINITION \"" + name + "\"");

		// se non c'è eseguo dal toolbox

		toolbox.exec(name, unboxedActualParameters);

	}

	@Override
	public String toString() {
		// return new ToStringCreator(getClass()).
		// append(name, actualParametersList).toString();
		return name + ArrayUtils.toString(actualParametersList);
	}
}
